<!-- Licensed under a BSD license. See license.html for license -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>Three.js - Align HTML Elements to 3D Globe</title>
    <style>
    html, body {
        height: 100%;
        margin: 0;
        font-family: sans-serif;
    }
    #c {
        width: 100%;  /* let our container decide our size */
        height: 100%;
        display: block;
    }
    #container {
      position: relative;  /* makes this the origin of its children */
      width: 100%;
      height: 100%;
      overflow: hidden;
    }
    #labels {
      position: absolute;  /* let us position ourself inside the container */
      z-index: 0;          /* make a new stacking context so children don't sort with rest of page */
      left: 0;             /* make our position the top left of the container */
      top: 0;
      color: white;
    }
    #labels>div {
      position: absolute;  /* let us position them inside the container */
      left: 0;             /* make their default position the top left of the container */
      top: 0;
      cursor: pointer;     /* change the cursor to a hand when over us */
      font-size: small;
      user-select: none;   /* don't let the text get selected */
      pointer-events: none;  /* make us invisible to the pointer */
      text-shadow:         /* create a black outline */
        -1px -1px 0 #000,
         0   -1px 0 #000,
         1px -1px 0 #000,
         1px  0   0 #000,
         1px  1px 0 #000,
         0    1px 0 #000,
        -1px  1px 0 #000,
        -1px  0   0 #000;
    }
    #labels>div:hover {
      color: red;
    }
    </style>
  </head>
  <body>
    <div id="container">
      <canvas id="c"></canvas>
      <div id="labels"></div>
    </div>
  </body>
<script type="importmap">
{
  "imports": {
    "three": "../../build/three.module.js",
    "three/addons/": "../../examples/jsm/"
  }
}
</script>

<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

function main() {

	const canvas = document.querySelector( '#c' );
	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );

	const fov = 60;
	const aspect = 2; // the canvas default
	const near = 0.1;
	const far = 10;
	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
	camera.position.z = 2.5;

	const controls = new OrbitControls( camera, canvas );
	controls.enableDamping = true;
	controls.enablePan = false;
	controls.minDistance = 1.2;
	controls.maxDistance = 4;
	controls.update();

	const scene = new THREE.Scene();
	scene.background = new THREE.Color( '#236' );

	{

		const loader = new THREE.TextureLoader();
		const texture = loader.load( 'resources/data/world/country-outlines-4k.png', render );
		const geometry = new THREE.SphereGeometry( 1, 64, 32 );
		const material = new THREE.MeshBasicMaterial( { map: texture } );
		scene.add( new THREE.Mesh( geometry, material ) );

	}

	async function loadJSON( url ) {

		const req = await fetch( url );
		return req.json();

	}

	let countryInfos;
	async function loadCountryData() {

		countryInfos = await loadJSON( 'resources/data/world/country-info.json' ); /* threejs.org: url */

		const lonFudge = Math.PI * 1.5;
		const latFudge = Math.PI;
		// these helpers will make it easy to position the boxes
		// We can rotate the lon helper on its Y axis to the longitude
		const lonHelper = new THREE.Object3D();
		// We rotate the latHelper on its X axis to the latitude
		const latHelper = new THREE.Object3D();
		lonHelper.add( latHelper );
		// The position helper moves the object to the edge of the sphere
		const positionHelper = new THREE.Object3D();
		positionHelper.position.z = 1;
		latHelper.add( positionHelper );

		const labelParentElem = document.querySelector( '#labels' );
		for ( const countryInfo of countryInfos ) {

			const { lat, lon, name } = countryInfo;

			// adjust the helpers to point to the latitude and longitude
			lonHelper.rotation.y = THREE.MathUtils.degToRad( lon ) + lonFudge;
			latHelper.rotation.x = THREE.MathUtils.degToRad( lat ) + latFudge;

			// get the position of the lat/lon
			positionHelper.updateWorldMatrix( true, false );
			const position = new THREE.Vector3();
			positionHelper.getWorldPosition( position );
			countryInfo.position = position;

			// add an element for each country
			const elem = document.createElement( 'div' );
			elem.textContent = name;
			labelParentElem.appendChild( elem );
			countryInfo.elem = elem;

		}

		requestRenderIfNotRequested();

	}

	loadCountryData();

	const tempV = new THREE.Vector3();

	function updateLabels() {

		// exit if we have not yet loaded the JSON file
		if ( ! countryInfos ) {

			return;

		}

		for ( const countryInfo of countryInfos ) {

			const { position, elem } = countryInfo;

			// get the normalized screen coordinate of that position
			// x and y will be in the -1 to +1 range with x = -1 being
			// on the left and y = -1 being on the bottom
			tempV.copy( position );
			tempV.project( camera );

			// convert the normalized position to CSS coordinates
			const x = ( tempV.x * .5 + .5 ) * canvas.clientWidth;
			const y = ( tempV.y * - .5 + .5 ) * canvas.clientHeight;

			// move the elem to that position
			elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;

			// set the zIndex for sorting
			elem.style.zIndex = ( - tempV.z * .5 + .5 ) * 100000 | 0;

		}

	}

	function resizeRendererToDisplaySize( renderer ) {

		const canvas = renderer.domElement;
		const width = canvas.clientWidth;
		const height = canvas.clientHeight;
		const needResize = canvas.width !== width || canvas.height !== height;
		if ( needResize ) {

			renderer.setSize( width, height, false );

		}

		return needResize;

	}

	let renderRequested = false;

	function render() {

		renderRequested = undefined;

		if ( resizeRendererToDisplaySize( renderer ) ) {

			const canvas = renderer.domElement;
			camera.aspect = canvas.clientWidth / canvas.clientHeight;
			camera.updateProjectionMatrix();

		}

		controls.update();

		updateLabels();

		renderer.render( scene, camera );

	}

	render();

	function requestRenderIfNotRequested() {

		if ( ! renderRequested ) {

			renderRequested = true;
			requestAnimationFrame( render );

		}

	}

	controls.addEventListener( 'change', requestRenderIfNotRequested );
	window.addEventListener( 'resize', requestRenderIfNotRequested );

}

main();
</script>
</html>
